import Complex from 'complex.js';
import type { FilterType } from './types';

// https://github.com/thi-ng/umbrella/tree/develop/packages/dsp
// TODO 未来可能使用这个包进行替换

// Coefficients class for filter calculations
class Coefficients {
  constructor(
    public first: number = 0.0,
    public second: number = 0.0,
    public third: number = 0.0,
  ) {}
}

// Filter parameters interface
export interface FilterParams {
  fs?: number; // The sampling frequency.
  fc?: number; // The cutoff frequency.
  gain?: number; // The gain.
  q?: number; // The Q factor.
  filterType?: FilterType; // The type of filter.
}

export class BiQuadFilter {
  private coeffsA: Coefficients = new Coefficients();
  private coeffsB: Coefficients = new Coefficients();
  private _fc: number;
  private _fs: number;
  private _gain: number;
  private _q: number;
  private _filterType: FilterType;

  // flag to indicate if coefficients need to be recalculated
  private _isDirty: boolean = true;

  constructor(params?: FilterParams) {
    const { fs, fc, gain, q, filterType } = params ?? {};

    this._fs = fs ?? 48000.0;
    this._fc = Math.min(fc ?? 48000.0, this._fs / 2.0);
    this._gain = gain ?? 1.0;
    this._q = q ?? 0.7;
    this._filterType = filterType ?? 'peaking';
  }

  // Getters and setters
  get fc(): number {
    return this._fc;
  }

  set fc(value: number) {
    if (value < 0) throw new Error('Frequency must be positive');

    const minFC = Math.min(value, this._fs / 2.0);
    if (this._fc !== minFC) {
      this._fc = minFC;
      this._isDirty = true;
    }
  }

  get fs(): number {
    return this._fs;
  }

  set fs(value: number) {
    if (value < 0) throw new Error('Sample rate must be positive');

    if (this._fs !== value) {
      this._fs = value;
      this._fc = Math.min(this._fc, value / 2.0);
      this._isDirty = true;
    }
  }

  get gain(): number {
    return this._gain;
  }

  set gain(value: number) {
    if (this._gain !== value) {
      this._gain = value;
      this._isDirty = true;
    }
  }

  get q(): number {
    return this._q;
  }

  set q(value: number) {
    if (this._q !== value) {
      this._q = value;
      this._isDirty = true;
    }
  }

  get filterType(): FilterType {
    return this._filterType;
  }

  set filterType(value: FilterType) {
    if (this._filterType !== value) {
      this._filterType = value;
      this._isDirty = true;
    }
  }

  // Ensure coefficients are up to date
  private ensureCoefficients(): void {
    if (this._isDirty) {
      this.calcCoeffs();
      this._isDirty = false;
    }
  }

  // Calculate complex gain at a specific frequency
  private calcComplexGain(freq: number): Complex {
    this.ensureCoefficients();
    const piFreqCalc = (2 * Math.PI * freq) / this._fs;
    const num = new Complex(
      this.coeffsB.first * Math.cos(0.0) +
        this.coeffsB.second * Math.cos(piFreqCalc) +
        this.coeffsB.third * Math.cos(2 * piFreqCalc),
      -this.coeffsB.first * Math.sin(0.0) -
        this.coeffsB.second * Math.sin(piFreqCalc) -
        this.coeffsB.third * Math.sin(2 * piFreqCalc),
    );
    const denom = new Complex(
      1.0 * Math.cos(0.0) +
        this.coeffsA.second * Math.cos(piFreqCalc) +
        this.coeffsA.third * Math.cos(2 * piFreqCalc),
      -1.0 * Math.sin(0.0) -
        this.coeffsA.second * Math.sin(piFreqCalc) -
        this.coeffsA.third * Math.sin(2 * piFreqCalc),
    );

    const gainDiv = denom.abs() ** 2;

    return new Complex(
      (num.re * denom.re + num.im * denom.im) / gainDiv,
      (num.im * denom.re - num.re * denom.im) / gainDiv,
    );
  }

  // Calculate gain in decibels at a specific frequency
  public calcDbGain(freq: number): number {
    return 20.0 * Math.log10(this.calcComplexGain(freq).abs());
  }

  // Calculate coefficients based on current filter type
  private calcCoeffs(): void {
    switch (this.filterType) {
      case 'lowpass':
        this.calcLowPassBiQuad();
        break;
      case 'highpass':
        this.calcHighPassBiQuad();
        break;
      case 'allpass':
        this.calcAllPassBiQuad();
        break;
      case 'lowshelf':
        this.calcLowShelfBiQuad();
        break;
      case 'highshelf':
        this.calcHighShelfBiQuad();
        break;
      case 'peaking':
        this.calcPeakingBiQuad();
        break;
    }
  }

  private calcBypassFilter(): void {
    this.coeffsA = new Coefficients(0.0, 0.0, 0.0);
    this.coeffsB = new Coefficients(1.0, 0.0, 0.0);
  }

  private calcLowPassBilinear(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const denom = wd + 1.0;

    this.coeffsA = new Coefficients(0.0, (wd - 1.0) / denom, 0.0);
    this.coeffsB = new Coefficients(wd / denom, wd / denom, 0.0);
  }

  private calcHighPassBilinear(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const denom = wd + 1.0;

    this.coeffsA = new Coefficients(0.0, (wd - 1.0) / denom, 0.0);
    this.coeffsB = new Coefficients(1.0 / denom, -1.0 / denom, 0.0);
  }

  private calcAllPassBilinear(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const denom = wd + 1.0;

    this.coeffsA = new Coefficients(0.0, (wd - 1.0) / denom, 0.0);
    this.coeffsB = new Coefficients((1.0 - wd) / denom, -1.0, 0.0);
  }

  private calcLowShelfBilinear(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const wd1 = wd * 10.0 ** (this.gain / 40.0);
    const wd2 = wd / 10.0 ** (this.gain / 40.0);
    const denom = wd2 + 1.0;

    this.coeffsA = new Coefficients(0.0, (wd2 - 1.0) / denom, 0.0);
    this.coeffsB = new Coefficients(
      (wd1 + 1.0) / denom,
      (wd1 - 1.0) / denom,
      0.0,
    );
  }

  private calcHighShelfBilinear(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const wd1 = wd / 10.0 ** (this.gain / 40.0);
    const wd2 = wd * 10.0 ** (this.gain / 40.0);
    const denom = wd2 + 1.0;

    this.coeffsA = new Coefficients(0.0, (wd2 - 1.0) / denom, 0.0);
    this.coeffsB = new Coefficients(
      (10.0 ** (this.gain / 20) * (wd1 + 1.0)) / denom,
      (10.0 ** (this.gain / 20) * (wd1 - 1.0)) / denom,
      0.0,
    );
  }

  private calcTiltBilinear(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const wd1 = wd / 10.0 ** (this.gain / 40.0);
    const wd2 = wd * 10.0 ** (this.gain / 40.0);
    const denom = wd2 + 1.0;

    this.coeffsA = new Coefficients(0.0, (wd2 - 1.0) / denom, 0.0);
    this.coeffsB = new Coefficients(
      (10.0 ** (this.gain / 40) * (wd1 + 1.0)) / denom,
      (10.0 ** (this.gain / 40) * (wd1 - 1.0)) / denom,
      0.0,
    );
  }

  private calcLowPassBiQuad(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const denom = wd * wd + wd / this._q + 1.0;

    this.coeffsA = new Coefficients(
      0.0,
      (2 * wd * wd - 2.0) / denom,
      (wd * wd - wd / this._q + 1.0) / denom,
    );

    this.coeffsB = new Coefficients(
      (wd * wd) / denom,
      (2.0 * wd * wd) / denom,
      (wd * wd) / denom,
    );
  }

  private calcHighPassBiQuad(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const denom = wd * wd + wd / this._q + 1.0;

    this.coeffsA = new Coefficients(
      0.0,
      (2 * wd * wd - 2.0) / denom,
      (wd * wd - wd / this._q + 1.0) / denom,
    );

    this.coeffsB = new Coefficients(1.0 / denom, -2.0 / denom, 1.0 / denom);
  }

  private calcAllPassBiQuad(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const denom = wd * wd + wd / this._q + 1.0;

    this.coeffsA = new Coefficients(
      0.0,
      (2 * wd * wd - 2.0) / denom,
      (wd * wd - wd / this._q + 1.0) / denom,
    );

    this.coeffsB = new Coefficients(
      (wd * wd - wd / this._q + 1.0) / denom,
      (2.0 * wd * wd - 2.0) / denom,
      1.0,
    );
  }

  private calcLowShelfBiQuad(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const wd1 = wd * 10.0 ** (this._gain / 80);
    const wd2 = wd / 10.0 ** (this._gain / 80);
    const denom = wd2 * wd2 + wd2 / this._q + 1.0;

    this.coeffsA = new Coefficients(
      0.0,
      (2.0 * wd2 * wd2 - 2.0) / denom,
      (wd2 * wd2 - wd2 / this._q + 1.0) / denom,
    );

    this.coeffsB = new Coefficients(
      (wd1 * wd1 + wd1 / this._q + 1.0) / denom,
      (2.0 * wd1 * wd1 - 2.0) / denom,
      (wd1 * wd1 - wd1 / this._q + 1.0) / denom,
    );
  }

  private calcHighShelfBiQuad(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const wd1 = wd / 10.0 ** (this._gain / 80);
    const wd2 = wd * 10.0 ** (this._gain / 80);
    const denom = wd2 * wd2 + wd2 / this._q + 1.0;

    this.coeffsA = new Coefficients(
      0.0,
      (2.0 * wd2 * wd2 - 2.0) / denom,
      (wd2 * wd2 - wd2 / this._q + 1.0) / denom,
    );

    this.coeffsB = new Coefficients(
      (10.0 ** (this._gain / 20) * (wd1 * wd1 + wd1 / this._q + 1.0)) / denom,
      (10.0 ** (this._gain / 20) * (2.0 * wd1 * wd1 - 2.0)) / denom,
      (10.0 ** (this._gain / 20) * (wd1 * wd1 - wd1 / this._q + 1.0)) / denom,
    );
  }

  private calcTiltBiQuad(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const wd1 = wd / 10.0 ** (this._gain / 80);
    const wd2 = wd * 10.0 ** (this._gain / 80);
    const denom = wd2 * wd2 + wd2 / this._q + 1.0;

    this.coeffsA = new Coefficients(
      0.0,
      (2.0 * wd2 * wd2 - 2.0) / denom,
      (wd2 * wd2 - wd2 / this._q + 1.0) / denom,
    );

    this.coeffsB = new Coefficients(
      (10.0 ** (this._gain / 40) * (wd1 * wd1 + wd1 / this._q + 1.0)) / denom,
      (10.0 ** (this._gain / 40) * (2.0 * wd1 * wd1 - 2.0)) / denom,
      (10.0 ** (this._gain / 40) * (wd1 * wd1 - wd1 / this._q + 1.0)) / denom,
    );
  }

  private calcPeakingBiQuad(): void {
    const wd = Math.tan((Math.PI * this._fc) / this._fs);
    const a =
      -1.0 / (2.0 * this._q) + Math.sqrt((1.0 / (2.0 * this._q)) ** 2.0 + 1.0);
    const aTerm = Math.tan((Math.PI * a * this._fc) / this._fs);
    const qdTop = aTerm * wd;
    const qdBottom = wd ** 2.0 - aTerm ** 2.0;
    const qd = qdTop / qdBottom;
    const q1 = qd / 10.0 ** (this._gain / 40.0);
    const q2 = qd * 10.0 ** (this._gain / 40.0);
    const denom = wd * wd + wd / q2 + 1.0;

    this.coeffsA = new Coefficients(
      0.0,
      (2 * wd * wd - 2.0) / denom,
      (wd * wd - wd / q2 + 1.0) / denom,
    );

    this.coeffsB = new Coefficients(
      (wd * wd + wd / q1 + 1.0) / denom,
      (2.0 * wd * wd - 2.0) / denom,
      (wd * wd - wd / q1 + 1.0) / denom,
    );
  }
}
